《一道 JS 面试题引发的思考》笔记和思考

这真是一篇牛逼的文章……读了一个多小时才读懂,得好好品味一下才好

JS 面试题原题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//比较下面两段代码,试述两段代码的不同之处
// A--------------------------
var scope = "global scope"
function checkscope() {
var scope = "local scope"
function f() {
return scope
}
return f()
}
checkscope()

// B---------------------------
var scope = "global scope"
function checkscope() {
var scope = "local scope"
function f() {
return scope
}
return f
}
checkscope()()

这两段代码其实都输出"local scope",考的是跟函数作用域链和闭包相关的知识。

其实这道题在《JavaScript 权威指南》第八章也出现过,当时只是略微理解了一下,大概讲的是,函数在定义的时候即会绑定到作用域链上,但是不是很深刻。这篇文章解释的异常详细,都让我觉得有些晦涩,所以要写点笔记。

变量对象(variable object)

Every execution context has associated with it a variable object. Variables and functions declared in the source text are added as properties of the variable object. For function code, parameters are added as properties of the variable object.

简言之就是:每一个执行上下文都会分配一个变量对象(variable object),变量对象的属性由 变量(variable) 和 函数声明(function declaration) 构成。在函数上下文情况下,参数列表(parameter list)也会被加入到变量对象(variable object)中作为属性。变量对象与当前作用域息息相关。不同作用域的变量对象互不相同,它保存了当前作用域的所有函数和变量。

好吧,文章中的简而言之真的非常而言之…

我自己理解了一下,变量对象就是一个保存当前作用域的东西,里面存了当前作用域的所有函数和变量(以属性的方式),但是这个叫变量对象的东西,是没法被程序调度起来的(除了全局变量对象,其实就是window对象)。

文中有两段神奇的代码值得研究一下:

1
2
3
// 函数声明
function a() {}
console.log(typeof a) // "function"
1
2
3
4
// 函数表达式
var a = function _a() {}
console.log(typeof a) // "function"
console.log(typeof _a) // "undefined"

这个区别其实在于:第二段代码,执行完之后,其实根本就没有_a这个东西了,具体可以看我在浏览器中执行的结果截图。用函数表达式来定义函数,《JavaScript 权威指南》书中讲到:定义函数表达式时并没有声明一个变量。所以,其实根本就没有声明_a这个变量,又怎么可能把它存入变量对象(variable object)中呢

活动对象(activation object)

When control enters an execution context for function code, an object called the activation object is created and associated with the execution context. The activation object is initialised with a property with name arguments and attributes { DontDelete }. The initial value of this property is the arguments object described below.
The activation object is then used as the variable object for the purposes of variable instantiation.

简言之:当函数被激活,那么一个活动对象(activation object)就会被创建并且分配给执行上下文。活动对象由特殊对象 arguments 初始化而成。随后,他被当做变量对象(variable object)用于变量初始化。

我的理解,活动对象,其实就是arguments。在函数执行的时候创建。文中有一段代码用来解释这个概念:

1
2
3
4
5
function a(name, age) {
var gender = "male"
function b() {}
}
a("k", 10)

不知道用下面这个图来解释它合不合适,但是这是我的理解。

在函数a调用的时候,会创建activation object,而activation object事实上就是arguments。之后函数a在执行的时候,会生成variable object,实际上就是作用域内的函数和对象再加上activation object组成的。

执行环境和作用域链(execution context and scope chain)

文中引入了一个叫环境栈的概念,其实很好理解,就是作用域链的另外一种表现形式而已。

我比较想画个图来帮助理解下面这段话:

1
2
3
4
5
function test(num) {
var a = "2"
return a + num
}
test(1)
  1. 执行流开始 初始化 function test,test 函数会维护一个私有属性 [[scope]],并使用当前环境的作用域链初始化,在这里就是 test.[[Scope]]=global scope.
  2. test 函数执行,这时候会为 test 函数创建一个执行环境,然后通过复制函数的[[Scope]]属性构建起 test 函数的作用域链。此时 test.scopeChain = [test.[[Scope]]]
  3. test 函数的活动对象被初始化,随后活动对象被当做变量对象用于初始化。即 test.variableObject = test.activationObject.contact[num,a] = [arguments].contact[num,a]
  4. test 函数的变量对象被压入其作用域链,此时 test.scopeChain = [ test.variableObject, test.[[scope]]];
  1. test函数的声明(初始化)。事实上会生成一个[[Scopes]]属性,作为test函数作用域链上的一层。我特地去浏览器中声明了这个函数,并且查看了它的属性,的确有[[Scopes]]这个属性。

  2. test函数开始执行。创建test函数内部的作用域,并添加到作用域链上。此时test函数的作用域链(黄色部分)就包含了[[Scopes]]属性:

  3. 在步骤 2 的同时,会用arguments初始化testactivation object(活动对象),并且用这个activation object(活动对象)来生成variable object(变量对象)。之后,将variable object(变量对象)添加到作用域链中。

  4. 动图解释一下:

    关于题目本身

    其实两段代码差别不大,区别在于,f()函数执行的环境不一样。花了一个多小时做了个动图,结果觉得还是算了。。。太麻烦了。。。

    因为 B 这段代码,执行完checkscope()函数之后,就从执行环境栈中弹出checkscope的执行环境,但是对结果不产生影响。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var scope = "global scope"
    function checkscope() {
    var scope = "local scope"
    function f() {
    return scope
    }
    return f
    }
    checkscope()()

    而 A 这段代码,在执行checkscope()的过程中执行了f(),此时checkscope的执行环境还未从执行环境栈中弹出。下面这幅图展示了这点区别,当 A 代码还在执行f()的时候,B 代码已经从栈中弹出了checkscope的作用域链。